Project Detroit
JavaにJavaScriptエンジン「V8」とPythonランタイム「CPython」を組み込む「Project Detroit」、オラクルが発表
Python側の詳細はこちら:openjdk/detroit-python: openjdk.org/projects/detroit
JavaScriptからJavaクラスを呼び出せる仕組み
大きく 3層のブリッジ構造 になっています。
1層目:JNI(Java Native Interface)で Java ↔ C++ をつなぐ
src/native/ には jvmv8_jni.cpp、jvmv8_java_classes.cpp などのC++ファイルが入っています。ここが Java と V8 エンジンを物理的につなぐ「接着剤」です。
Java の native メソッドとして宣言された関数は、JNI を通じて C++ の実装に委譲されます。V8 は C++ で動いているので、JNI を経由することで「Java → C++ → V8」という方向と「V8 → C++ → Java」という方向の両方向の呼び出しが可能になります。
2層目:Java.type() と JavaImporter — JS 側の API
サンプルコードを見ると、JavaScript 側では次のような書き方ができます。
code:javascript
// Javaの型をJSの変数として取得する
var Runnable = Java.type("java.lang.Runnable");
var Thread = Java.type("java.lang.Thread");
// JavaScriptのオブジェクト記法でJavaインターフェースを実装する
var r = new Runnable({
run: function() {
print("I am a runnable " + Thread.currentThread());
}
});
r.run();
code:javascript
// JavaImporter でパッケージ単位にインポートすることもできる
with (new JavaImporter(java.io, java.net, java.lang.StringBuilder)) {
var buf = new StringBuilder();
var u = new URL("https://dev.java/");
// ... java.io や java.net のクラスが使い放題
}
code:javascript
// java.lang.System などはドット記法でそのままアクセス可能
var console = java.lang.System.console();
この Java.type() や JavaImporter などのグローバル関数は、V8GlobalFunctions.java で定義され、V8 の起動時に JavaScript のグローバルスコープに注入されます。
3層目:リフレクション + コード生成でクラスをラップ
V8ClassGenerator.java と AccessibleMembersLookup.java が核心部分です。Java.type("java.lang.Thread") が呼ばれると、内部では次のことが起きます。
1. Javaリフレクション(Class.forName() など)で対象クラスのメタ情報(メソッド、コンストラクタ、フィールド)を動的に収集する
2. そのクラスに対応する V8のオブジェクトテンプレート(JavaScript オブジェクトの「型」に相当するもの)を C++ レベルで生成する
3. 各メソッドに対応する コールバック関数 を V8 に登録し、JSから呼ばれたらJNI経由でJavaのメソッドを実行するようにする
つまり「Javaクラスの情報をリフレクションで読み取り、V8のオブジェクトとして見えるようにラップする」というアプローチです。
逆方向:JavaScriptで実装したインターフェースをJavaに渡す
V8InterfaceImplementor.java がこれを担当します。上の interface_impl.js の例でいうと、JSのオブジェクト { run: function(){...} } を java.lang.Runnable として扱えるように、動的プロキシ(java.lang.reflect.Proxy)を使ってJava側のインターフェースを実装したラッパーオブジェクトを生成します。
まとめ:データの流れ
code:_
JavaScript (V8)
↕ V8 Object Template / Callback
C++ ブリッジ層 (jvmv8_*.cpp)
↕ JNI (Java Native Interface)
Java (JVM)
↕ Reflection / Dynamic Proxy
Javaクラス・インターフェース
要するに、C++ が JVM と V8 の両方に対して「通訳」として機能し、リフレクションとコード生成によってJavaの型体系をそのままJavaScript側に透過的に見せる、という設計です。GraalVM の Polyglot API に近い発想ですが、こちらは純粋に V8 ベースで実現しているのが特徴です。
JNIの警告問題と対応
警告が出るようになった経緯
JEP 472「Prepare to Restrict the Use of JNI」(JDK 24でデリバリー)により、JNI を使うコードは実行時に警告が出るようになりました。
具体的には、以下のいずれかを行うと警告が発生します。
System.loadLibrary() / System.load() / Runtime.loadLibrary() でネイティブライブラリを読み込む
native キーワードをつけたメソッドを宣言する(JNI のダウンコール)
警告を抑制するには、Java 起動時に --enable-native-access=<モジュール名> というオプションを明示的に渡す必要があります。将来のJDKではこのオプションがないと警告ではなく例外になる予定です("integrity by default" という方針)。
detroit-js の対応
jjs.sh(起動スクリプト)を見ると、51行目にきちんと対応が入っています。
code:bash
java \
--module-path $lib \
--add-modules org.openjdk.engine.javascript \
--enable-native-access=org.openjdk.engine.javascript \ ← ここ
org.openjdk.engine.javascript.Main "$@"
--enable-native-access=org.openjdk.engine.javascript を指定することで、org.openjdk.engine.javascript モジュールに対してのみネイティブアクセスを許可し、警告を抑制しています。
また module-info.java を見ると、このプロジェクトはJPMS(Java Platform Module System)のモジュールとして正しく定義されているため、ALL-UNNAMED(全クラスパスに許可する乱暴な方法)ではなく特定モジュールだけに絞った最小権限の指定ができています。
まとめ
項目内容JNI警告の仕様JDK 24 (JEP 472) から --enable-native-access なしで警告将来の方針警告→例外へと段階的に強化予定detroit-jsの対応jjs.sh で --enable-native-access=org.openjdk.engine.javascript を指定済み設計の良い点モジュール単位で最小限の許可に絞っている
Panamaは使われていない
java.lang.foreign(FFM APIのパッケージ)を検索すると0件、MemorySegmentも0件です。V8.java のimport文を見ても、java.io.*、java.nio.*、java.lang.reflect.* などは使われていますが、Panamaのクラスは一切ありません。
代わりに採用している方式は正統派JNIで、コードを見ると:
System.load() でネイティブライブラリ(.so / .dylib)を読み込む
private native static long createIsolate0(...) のように native キーワードで宣言したメソッドをC++で実装する
という古典的なJNIアプローチです。
なぜPanamaではなくJNIを選んだか(推測)
V8との連携にFFM APIを使わない理由はいくつか考えられます。
技術的な制約として、FFM APIはJavaからCの関数を呼ぶ「ダウンコール」と、CからJavaのコードを呼ぶ「アップコール」の両方をサポートしていますが、V8との連携では「C++(V8)側からJavaのメソッドをコールバックとして呼ぶ」パターンが多用されます。このような複雑な双方向のコールバック連携は、JNIのほうが成熟した仕組みとして実績があります。
実績と移行コストとして、Nashorn(前世代)もJNIベースだったため、エンジニアの知見やコードパターンがJNIに蓄積されています。また、V8のC++ APIを使う上でJNIはC++との親和性が高く、JNIEnv* を通じてJavaオブジェクトを自由に操作できます。
FFM APIのトレードオフとして、FFM APIはCとのやりとりには強い一方、複雑なC++オブジェクトとの相互運用(V8のような高機能なC++ライブラリ)には向いていません。V8のAPIはC++のクラス・テンプレート・コールバックを多用するため、FFMよりJNIのほうが自然なマッピングになります。
ただし、JEP 472が示す「将来的にJNIを制限する」という方向性を踏まえると、将来のバージョンではFFM APIへの移行が検討される可能性はあります。今のところdetroit-jsは「JNI使用に明示的な許可を与える」という現行の緩和策(--enable-native-access)で対応している状態です。